Repository navigation
OpenConceptLab/ocl_online#425 | The announcement banner shows the deployment's ANNOUNCEMENT setting - #121
Conversation
Codex review, pass 1 (codex-cli 0.166.0, commit 41813d3)Verdict: findings (1).
Response
findings.json{
"reviewer": "codex-cli 0.166.0",
"perspective": "defects",
"mode": "full",
"commit": "41813d337b5073fa786ba68b556dce3e9870fa61",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "F1.1",
"severity": "P1",
"tag": null,
"file": "src/components/app/AnnouncementBanner.jsx",
"line": 22,
"claim": "The banner does not automatically disappear at its until time.",
"scenario": "A user opens the app before until and keeps the page open past it. Expiry is checked only during rendering, with no timer triggering another render. A mounted-component reproduction confirmed that the expired banner and its layout offset remain until an unrelated render occurs.",
"action": "Schedule a state update when until is reached, clean up the timer on unmount, and add a component test that advances past expiry without manually rerendering.",
"lines_delta": null,
"confidence": "high"
}
]
} |
Codex review, pass 2 (codex-cli 0.166.0, commit a7e6d49)Verdict: clean. F1.1 resolved. findings.json{
"reviewer": "codex-cli 0.166.0",
"perspective": "defects",
"mode": "verify",
"commit": "a7e6d4995e76ec3479a14fd9a5b82dc6f7bd6ef1",
"verdict": "clean",
"prior": [
{
"id": "F1.1",
"status": "resolved"
}
],
"findings": []
} |
Simplification review (claude subagent, commit c55b3f9c in oclweb3; it covered all three repos)
Applied in e764e29. Tests rerun: green. simplify.json{
"reviewer": "claude subagent",
"perspective": "simplify",
"mode": "full",
"commit": "c55b3f9cbf1323306a64a7455f3e3499d5d83262",
"verdict": "findings",
"prior": [
{
"id": "F1.1",
"status": "resolved"
},
{
"id": "F1.2",
"status": "resolved"
}
],
"findings": [
{
"id": "S1",
"severity": null,
"tag": "delete",
"file": "oclweb3/src/common/announcement.js",
"line": 37,
"claim": "All three copies: the opening guard `if(typeof raw !== 'string' || !raw.trim()) return null` is redundant. The JSON.parse try/catch right below it already returns null for undefined, '', whitespace and any non-JSON value (JSON.parse coerces them and throws). isObject then rejects null, numbers and other non-object JSON. The validation stays; only the duplicate check goes.",
"scenario": null,
"action": "Nothing replaces it. I checked this on a scratch copy: the 'no setting: no banner' test and the other 17 still pass.",
"lines_delta": -2,
"confidence": "high"
},
{
"id": "S2",
"severity": null,
"tag": "shrink",
"file": "oclweb3/src/common/announcement.js",
"line": 52,
"claim": "All three copies: the links loop runs isObject(link) twice and builds the array by hand. localize already returns null for a label that isn't an object, and httpsURL returns null for a missing url, so `link?.` covers links that are strings, null or arrays. The file already uses `?.` (language?.split, announcement?.until), so this is safe for oclweb2's ESLint 4 + babel-eslint.",
"scenario": null,
"action": "Replace the 8-line loop with:\n const links = setting.links.map(link => ({label: localize(link?.label, language), url: httpsURL(link?.url)}))\n if(links.some(link => !link.label || !link.url))\n return null\nAll link tests pass on a scratch copy.",
"lines_delta": -5,
"confidence": "medium"
},
{
"id": "S3",
"severity": null,
"tag": "yagni",
"file": "oclweb3/src/common/announcement.js",
"line": 36,
"claim": "All three copies: the `now = Date.now()` defaults on getAnnouncement and msUntilExpiry, and the `?.` on `announcement` in msUntilExpiry, guard cases no caller produces. AnnouncementBanner is the only caller. It always passes now and calls msUntilExpiry only when `open`, which means announcement is not null. Removing the `?.` also means removing the one-line `assert.equal(msUntilExpiry(null), null)` (test line 97). The assertions that pin F1.1 stay untouched: the one-hour delay, null for no until, and the re-check loop test.",
"scenario": null,
"action": "Use `(raw, language, now)` and `(announcement, now) => announcement.until ? ... : null`, and delete the msUntilExpiry(null) assertion. 18/18 pass on a scratch copy.",
"lines_delta": -1,
"confidence": "low"
},
{
"id": "S4",
"severity": null,
"tag": "dup-test",
"file": "oclweb3/src/common/__tests__/announcement.test.js",
"line": 168,
"claim": "All three copies: 'a setting cannot end the string and run script in env-config.js' repeats the checks of the round-trip test above it. That test already asserts Object.keys(context) is exactly ['ANNOUNCEMENT'] and the value reads back unchanged, and any payload that ends the string fails both checks. Merging keeps the same injection coverage with all four payloads.",
"scenario": null,
"action": "Move its four payloads into the round-trip test's value list and delete the separate test. On a scratch copy I removed each of js_string's four escape cases (\\\\, \\\", \\n, \\r) one at a time, and the merged test still failed every time.",
"lines_delta": -8,
"confidence": "medium"
},
{
"id": "S5",
"severity": null,
"tag": "dup-test",
"file": "oclweb3/src/common/__tests__/announcement.test.js",
"line": 118,
"claim": "All three copies: in the language test, the 9-line deepEqual for 'es' re-pins id, until and both URLs, which 'a valid setting shows its text and links' already covers. The only new behaviour it checks is the translated text and labels, including the en fallback for the second link's label.",
"scenario": null,
"action": "Replace it with:\n assert.equal(announce(SETTING, 'es').text, 'Saturday maintenance, in Spanish.')\n assert.deepEqual(announce(SETTING, 'es').links.map(link => link.label), ['Details, in Spanish', 'Contact us'])",
"lines_delta": -7,
"confidence": "medium"
},
{
"id": "S6",
"severity": null,
"tag": "shrink",
"file": "oclweb3/src/components/app/AnnouncementBanner.jsx",
"line": 11,
"claim": "All three copies (each one names the other two apps): the 4-line header comment repeats what announcement.js's header comment and the README already say: unset, malformed or expired means nothing shows; text is plain; a dismissal lasts until the id changes.",
"scenario": null,
"action": "Two lines: `// This deployment's announcement (common/announcement.js; README, \"Announcement banner\"),\n// fixed above the app bar. Same component in the Mapper and TBv2.`",
"lines_delta": -2,
"confidence": "low"
}
]
} |
Codex check of commits a7e6d49..e764e29 (codex-cli 0.166.0)Verdict: clean. The simplification commit broke nothing. findings.json{
"reviewer": "codex-cli 0.166.0",
"perspective": "defects",
"mode": "final",
"commit": "e764e292bc9faea30d0116d18859fb4440f18409",
"verdict": "clean",
"prior": [],
"findings": []
} |
Codex check of commits e764e29..663e989 (codex-cli 0.166.0)An adversarial review (claude subagent) tried to break the escaping and the checks. No input added script to Verdict: clean. That commit broke nothing. findings.json{
"reviewer": "codex-cli 0.166.0",
"perspective": "defects",
"mode": "final",
"commit": "663e989ac2df24d0f181a3a17891b3a102c39cde",
"verdict": "clean",
"prior": [
{
"id": "A1",
"status": "resolved"
},
{
"id": "A2",
"status": "resolved"
}
],
"findings": []
} |
…ent's ANNOUNCEMENT setting The banner's text and links come from an optional ANNOUNCEMENT environment variable (JSON), which start-prod.sh writes into env-config.js as an escaped string when the container starts. The app parses and checks it: plain text, https links only, the user's language with en as the fallback, hidden after `until`, and dismissed by `id`. Unset or invalid, no banner shows. Removes the announcement written into the app: its copy, link constants and locale strings. README documents the setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaZ4qVYj2Kwzt3oKZf1AwU
…ge left open, and links is required The banner checks the setting again when `until` arrives, so it goes away without a reload. `links` is required, as in the setting's shape; `[]` means no links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaZ4qVYj2Kwzt3oKZf1AwU
…hecks From the simplification review: JSON.parse's catch covers an unset or empty setting, the links check maps and tests in one pass, the unused `now` defaults go, and the language test and the component comment are shorter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaZ4qVYj2Kwzt3oKZf1AwU
…banner goes within a minute From the adversarial review: a long label (a pasted URL, say) no longer pushes the close button off a phone screen, because the text wraps anywhere and the button keeps its size. The banner checks `until` at least once a minute, so a timer that a sleeping laptop or a clock change delays can't keep it up. README: `until` needs Z or an offset, and the npm start example works with an apostrophe in the text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaZ4qVYj2Kwzt3oKZf1AwU
663e989 to
59b3b7e
Compare
Codex check of the rebase (codex-cli 0.166.0, commit d42da507)#122 merged first, so this branch is rebased onto main (6987b27); it was 663e989. There was one conflict, in Checked on 59b3b7e: CI's Codex checked this branch rebased onto #122's head before its last two commits (14d0134, rebased head d42da507), asking only whether the rebase broke anything: the conflict, the files git merged on its own, and how the banner's Verdict: clean. The rebase broke nothing. findings.json{
"reviewer": "codex-cli 0.166.0",
"perspective": "defects",
"mode": "final",
"commit": "d42da507dbc96b377dc444110d92652e8ecacb35",
"verdict": "clean",
"prior": [],
"findings": []
} |
The announcement banner above the app now shows only what the deployment sets in an optional
ANNOUNCEMENTenvironment variable. When it's unset, no banner shows, so each deployment runs its own banner or none. Same change in TBv3 (OpenConceptLab/oclweb3#88) and TBv2 (OpenConceptLab/oclweb2#61).What changed
start-prod.shwritesANNOUNCEMENTintoenv-config.jswhen the container starts, as an escaped JavaScript string, so a value can't end the string or add script. It escapes one character at a time, which behaves the same in every bash (${s//…}replacement handling changed in bash 5.2).src/common/announcement.js(new, the same file in all three apps) parses and checks the setting. Text and labels are plain text; links must behttps:; the user's language falls back toen; the banner hides afteruntilwith no restart; a dismissal is remembered byid. Invalid JSON, a missing field or a link that isn'thttps:means no banner.AnnouncementBanneronly displays what that returns. It keeps the layout (--announcement-height, the Header offset), and long text now wraps instead of running off a narrow screen. The close button's label is nowcommon.close.announcement.*locale strings.docker-compose.ymlpassesANNOUNCEMENTthrough.{ "id": "maintenance-2026-11", "text": {"en": "Planned maintenance on Saturday, 02:00-04:00 UTC.", "es": "Mantenimiento programado el sábado, de 02:00 a 04:00 UTC."}, "links": [{"label": {"en": "Details", "es": "Detalles"}, "url": "https://status.example.org/"}], "until": "2026-11-09T00:00:00Z" }Tests
src/common/__tests__/announcement.test.js(19 tests): unset; valid; invalid JSON and JSON that isn't an object; missing or malformed fields; links that aren'thttps:(http:,javascript:,data:, relative); expired and malformeduntil, and the re-check that hides a banner atuntil; other languages and theenfallback; dismissal, including blocked storage; andstart-prod.sh's escaping (values read back unchanged, and attempts to end the string don't run)../node_modules/eslint/bin/eslint.js src --ext .jsx,.jsis clean;npm run test:allpasses (631 unit, 11 integration).announcement.jsandstart-prod.shcode.Review
Codex (codex-cli 0.166.0, reasoning xhigh), up to 3 passes:
until(P1). Fixed: the banner checks again whenuntilarrives.linksis now required too, from the oclweb3 review.js_stringin bash 3.2, 5.1 (the image's), 5.2 and 5.3. It found two things, both fixed: a long label pushed the close button off a phone screen (now the text wraps and the button keeps its size), and a timer delayed by a sleeping laptop could keep an ended banner up (nowuntilis checked at least once a minute). Codex check of that commit: clean.Deploying
After this release, the banner shows only where a deployment sets
ANNOUNCEMENT. Where it's unset, the app shows none.Refs OpenConceptLab/ocl_online#425
🤖 Generated with Claude Code
https://claude.ai/code/session_01AaZ4qVYj2Kwzt3oKZf1AwU